/*
 * Copyright (C) 2014-2020 Arpit Khurana <arpitkh96@gmail.com>, Vishal Nehra <vishalmeham2@gmail.com>,
 * Emmanuel Messulam<emmanuelbendavid@gmail.com>, Raymond Lai <airwave209gt at gmail.com> and Contributors.
 *
 * This file is part of Amaze File Manager.
 *
 * Amaze File Manager is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.example.filemanager;


import static com.example.filemanager.constants.PreferencesConstants.PREFERENCE_TABLE_NAME;
import static com.example.filemanager.filesystem.smb.CifsContexts.SMB_URI_PREFIX;

import android.app.Application;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.preference.PreferenceManager;
import android.text.format.Formatter;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import com.amaze.filemanager.fileoperations.exceptions.CloudPluginException;
import com.amaze.filemanager.fileoperations.exceptions.ShellNotRunningException;
import com.amaze.filemanager.fileoperations.filesystem.OpenMode;
import com.amaze.filemanager.fileoperations.filesystem.root.NativeOperations;
import com.example.filemanager.command.DeleteFileCommand;
import com.example.filemanager.command.ListFilesCommand;
import com.example.filemanager.constants.PreferencesConstants;
import com.example.filemanager.data.LayoutElementParcelable;
import com.example.filemanager.files.FileUtils;
import com.example.filemanager.files.GenericCopyUtil;
import com.example.filemanager.filesystem.DeleteOperation;
import com.example.filemanager.filesystem.ExternalSdCardOperation;
import com.example.filemanager.utils.Applications;
import com.example.filemanager.utils.DataUtils;
import com.example.filemanager.utils.FileUtil;
import com.example.filemanager.utils.OTGUtil;
import com.example.filemanager.utils.RootHelper;
import com.example.filemanager.utils.SmbUtil;
import com.example.filemanager.utils.Utils;
import com.example.filemanager.utils.inter.OnFileFound;

import net.schmizz.sshj.sftp.SFTPClient;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import io.reactivex.Single;
import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import jcifs.smb.SmbException;
import jcifs.smb.SmbFile;

/** Hybrid file for handeling all types of files */
public class HybridFile {


  public static final String DOCUMENT_FILE_PREFIX =
      "content://com.android.externalstorage.documents";

  protected String path;
  protected OpenMode mode;
  protected String name;

  private final DataUtils dataUtils = DataUtils.getInstance();

  public HybridFile(OpenMode mode, String path) {
    this.path = path;
    this.mode = mode;
  }

  public HybridFile(OpenMode mode, String path, String name, boolean isDirectory) {
    this(mode, path);
    this.name = name;
    if (path.startsWith(SMB_URI_PREFIX) || isSmb() || isDocumentFile() || isOtgFile()) {
      Uri.Builder pathBuilder = Uri.parse(this.path).buildUpon().appendEncodedPath(name);
      if ((path.startsWith(SMB_URI_PREFIX) || isSmb()) && isDirectory) {
        pathBuilder.appendEncodedPath("/");
      }
      this.path = pathBuilder.build().toString();
    } else if (isRoot() && path.equals("/")) {
      // root of filesystem, don't concat another '/'
      this.path += name;
    } else {
      this.path += "/" + name;
    }
  }

  public void generateMode(Context context) {
    if (path.startsWith(SMB_URI_PREFIX)) {
      mode = OpenMode.SMB;
    } else if (path.startsWith(OTGUtil.PREFIX_OTG)) {
      mode = OpenMode.OTG;
    } else if (path.startsWith(DOCUMENT_FILE_PREFIX)) {
      mode = OpenMode.DOCUMENT_FILE;
    } else if (isCustomPath()) {
      mode = OpenMode.CUSTOM;
    } else if (context == null) {
      mode = OpenMode.FILE;
    } else {
      boolean rootmode =
          PreferenceManager.getDefaultSharedPreferences(context)
              .getBoolean(PreferencesConstants.PREFERENCE_ROOTMODE, false);
      if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
        mode = OpenMode.FILE;
        if (rootmode && !getFile().canRead()) {
          mode = OpenMode.ROOT;
        }
      } else {
        if (ExternalSdCardOperation.isOnExtSdCard(getFile(), context)) {
          mode = OpenMode.FILE;
        } else if (rootmode && !getFile().canRead()) {
          mode = OpenMode.ROOT;
        }

        // In some cases, non-numeric path is passed into HybridFile while mode is still
        // CUSTOM here. We are forcing OpenMode.FILE in such case too. See #2225
        if (OpenMode.UNKNOWN.equals(mode) || OpenMode.CUSTOM.equals(mode)) {
          mode = OpenMode.FILE;
        }
      }
    }
  }

  public void setMode(OpenMode mode) {
    this.mode = mode;
  }

  public OpenMode getMode() {
    return mode;
  }

  public void setPath(String path) {
    this.path = path;
  }

  public boolean isLocal() {
    return mode == OpenMode.FILE;
  }

  public boolean isRoot() {
    return mode == OpenMode.ROOT;
  }

  public boolean isSmb() {
    return mode == OpenMode.SMB;
  }

  public boolean isSftp() {
    return mode == OpenMode.SFTP;
  }

  public boolean isOtgFile() {
    return mode == OpenMode.OTG;
  }

  public boolean isDocumentFile() {
    return mode == OpenMode.DOCUMENT_FILE;
  }

  public boolean isBoxFile() {
    return mode == OpenMode.BOX;
  }

  public boolean isDropBoxFile() {
    return mode == OpenMode.DROPBOX;
  }

  public boolean isOneDriveFile() {
    return mode == OpenMode.ONEDRIVE;
  }

  public boolean isGoogleDriveFile() {
    return mode == OpenMode.GDRIVE;
  }

  public boolean isCloudDriveFile() {
    return isBoxFile() || isDropBoxFile() || isOneDriveFile() || isGoogleDriveFile();
  }

  @Nullable
  public File getFile() {
    return new File(path);
  }

  @Nullable
  public DocumentFile getDocumentFile(boolean createRecursive) {
    return OTGUtil.getDocumentFile(
        path,
        SafRootHolder.getUriRoot(),
            Applications.context(),
        OpenMode.DOCUMENT_FILE,
        createRecursive);
  }

  HybridFileParcelable generateBaseFileFromParent() {
    ArrayList<HybridFileParcelable> arrayList =
        RootHelper.getFilesList(getFile().getParent(), true, true);
    for (HybridFileParcelable baseFile : arrayList) {
      if (baseFile.getPath().equals(path)) return baseFile;
    }
    return null;
  }

  public long lastModified() {
    switch (mode) {
      case SMB:
        SmbFile smbFile = getSmbFile();
        if (smbFile != null) {
          try {
            return smbFile.lastModified();
          } catch (SmbException e) {
            return 0;
          }
        }
        break;
      case FILE:
        return getFile().lastModified();
      case DOCUMENT_FILE:
        return getDocumentFile(false).lastModified();
      case ROOT:
        HybridFileParcelable baseFile = generateBaseFileFromParent();
        if (baseFile != null) return baseFile.getDate();
    }
    return new File("/").lastModified();
  }

  /** Helper method to find length */
  public long length(Context context) {
    long s = 0L;
    switch (mode) {
      case SMB:
        s =
            Single.fromCallable(
                    () -> {
                      SmbFile smbFile = getSmbFile();
                      if (smbFile != null) {
                        try {
                          return smbFile.length();
                        } catch (SmbException e) {
                          return 0L;
                        }
                      } else {
                        return 0L;
                      }
                    })
                .subscribeOn(Schedulers.io())
                .blockingGet();
        return s;
      case FILE:
        s = getFile().length();
        return s;
      case ROOT:
        HybridFileParcelable baseFile = generateBaseFileFromParent();
        if (baseFile != null) return baseFile.getSize();
        break;
      case DOCUMENT_FILE:
        s = getDocumentFile(false).length();
        break;
      case OTG:
        s = OTGUtil.getDocumentFile(path, context, false).length();
        break;
      case DROPBOX:
      case BOX:
      case ONEDRIVE:
      default:
        break;
    }
    return s;
  }

  public String getPath() {
    return path;
  }

  public String getSimpleName() {
    String name = null;
    switch (mode) {
      case SMB:
        SmbFile smbFile = getSmbFile();
        if (smbFile != null) return smbFile.getName();
        break;
      default:
        StringBuilder builder = new StringBuilder(path);
        name = builder.substring(builder.lastIndexOf("/") + 1, builder.length());
    }
    return name;
  }

  public String getName(Context context) {
    switch (mode) {
      case SMB:
        SmbFile smbFile = getSmbFile();
        if (smbFile != null) {
          return smbFile.getName();
        }
        return null;
      case FILE:
      case ROOT:
        return getFile().getName();
      case OTG:
        if (!Utils.isNullOrEmpty(name)) {
          return name;
        }
        return OTGUtil.getDocumentFile(path, context, false).getName();
      case DOCUMENT_FILE:
        if (!Utils.isNullOrEmpty(name)) {
          return name;
        }
        return OTGUtil.getDocumentFile(
                path, SafRootHolder.getUriRoot(), context, OpenMode.DOCUMENT_FILE, false)
            .getName();
      default:
        if (path.isEmpty()) {
          return "";
        }

        String _path = path;
        if (path.endsWith("/")) {
          _path = path.substring(0, path.length() - 1);
        }

        int lastSeparator = _path.lastIndexOf('/');

        return _path.substring(lastSeparator + 1);
    }
  }

  public SmbFile getSmbFile(int timeout) {
    try {
      SmbFile smbFile = SmbUtil.create(path);
      smbFile.setConnectTimeout(timeout);
      return smbFile;
    } catch (MalformedURLException e) {
      return null;
    }
  }

  public SmbFile getSmbFile() {
    try {
      return SmbUtil.create(path);
    } catch (MalformedURLException e) {
      return null;
    }
  }

  public boolean isCustomPath() {
    return path.equals("0")
        || path.equals("1")
        || path.equals("2")
        || path.equals("3")
        || path.equals("4")
        || path.equals("5")
        || path.equals("6");
  }

  /** Helper method to get parent path */
  public String getParent(Context context) {
    switch (mode) {
      case SMB:
        SmbFile smbFile = getSmbFile();
        if (smbFile != null) {
          return smbFile.getParent();
        }
        return "";
      case FILE:
      case ROOT:
        return getFile().getParent();
      case SFTP:
      default:
        if (path.length() == getName(context).length()) {
          return null;
        }

        int start = 0;
        int end = path.length() - getName(context).length() - 1;

        return path.substring(start, end);
    }
  }

  public String getParentName() {
    StringBuilder builder = new StringBuilder(path);
    StringBuilder parentPath =
        new StringBuilder(builder.substring(0, builder.length() - (getSimpleName().length() + 1)));
    String parentName = parentPath.substring(parentPath.lastIndexOf("/") + 1, parentPath.length());
    return parentName;
  }

  /**
   * Whether this object refers to a directory or file, handles all types of files
   *
   * @deprecated use {@link #isDirectory(Context)} to handle content resolvers
   */
  public boolean isDirectory() {
    boolean isDirectory;
    switch (mode) {
      case SFTP:
        return isDirectory(Applications.context());
      case SMB:
        SmbFile smbFile = getSmbFile();
        try {
          isDirectory = smbFile != null && smbFile.isDirectory();
        } catch (SmbException e) {
          isDirectory = false;
        }
        break;
      case FILE:
        isDirectory = getFile().isDirectory();
        break;
      case ROOT:
        isDirectory = NativeOperations.isDirectory(path);
        break;
      case DOCUMENT_FILE:
        return getDocumentFile(false).isDirectory();
      case OTG:
        // TODO: support for this method in OTG on-the-fly
        // you need to manually call {@link RootHelper#getDocumentFile() method
        isDirectory = false;
        break;
      default:
        isDirectory = getFile().isDirectory();
        break;
    }
    return isDirectory;
  }

  public boolean isDirectory(Context context) {
    boolean isDirectory = false;
    switch (mode) {
      case FILE:
        isDirectory = getFile().isDirectory();
        break;
      case ROOT:
        isDirectory = NativeOperations.isDirectory(path);
        break;
      case DOCUMENT_FILE:
        isDirectory = getDocumentFile(false).isDirectory();
        break;
      case OTG:
        isDirectory = OTGUtil.getDocumentFile(path, context, false).isDirectory();
        break;
      case DROPBOX:
      case BOX:
      case GDRIVE:
      default:
        isDirectory = getFile().isDirectory();
        break;
    }
    return isDirectory;
  }

  /**
   * @deprecated use {@link #folderSize(Context)}
   */
  public long folderSize() {
    long size = 0L;

    switch (mode) {
      case FILE:
        size = FileUtils.folderSize(getFile(), null);
        break;
      case ROOT:
        HybridFileParcelable baseFile = generateBaseFileFromParent();
        if (baseFile != null) size = baseFile.getSize();
        break;
      default:
        return 0L;
    }
    return size;
  }

  /** Helper method to get length of folder in an otg */
  public long folderSize(Context context) {

    long size = 0l;

    switch (mode) {
      case FILE:
        size = FileUtils.folderSize(getFile(), null);
        break;
      case ROOT:
        HybridFileParcelable baseFile = generateBaseFileFromParent();
        if (baseFile != null) size = baseFile.getSize();
        break;
      case OTG:
        size = FileUtils.otgFolderSize(path, context);
        break;
      case DOCUMENT_FILE:
        final AtomicLong totalBytes = new AtomicLong(0);
        OTGUtil.getDocumentFiles(
            SafRootHolder.getUriRoot(),
            path,
            context,
            OpenMode.DOCUMENT_FILE,
            file -> totalBytes.addAndGet(FileUtils.getBaseFileSize(file, context)));
        break;
      case DROPBOX:
      case BOX:
      case GDRIVE:
      default:
        return 0l;
    }
    return size;
  }

  /** Gets usable i.e. free space of a device */
  public long getUsableSpace() {
    long size = 0L;
    switch (mode) {
      case FILE:
      case ROOT:
        size = getFile().getUsableSpace();
        break;
      case DROPBOX:
      case BOX:
      case GDRIVE:
      case DOCUMENT_FILE:
        size = FileProperties.getDeviceStorageRemainingSpace(SafRootHolder.INSTANCE.getVolumeLabel());
        break;
      case OTG:
        // TODO: Get free space from OTG when {@link DocumentFile} API adds support
        break;
    }
    return size;
  }

  /** Gets total size of the disk */
  public long getTotal(Context context) {
    long size = 0l;
    switch (mode) {
      case FILE:
      case ROOT:
        size = getFile().getTotalSpace();
        break;
      case DROPBOX:
      case BOX:
      case ONEDRIVE:
      case OTG:
        // TODO: Find total storage space of OTG when {@link DocumentFile} API adds support
        DocumentFile documentFile = OTGUtil.getDocumentFile(path, context, false);
        size = documentFile.length();
        break;
      case DOCUMENT_FILE:
        size = getDocumentFile(false).length();
        break;
    }
    return size;
  }

  /** Helper method to list children of this file */
  public void forEachChildrenFile(Context context, boolean isRoot, OnFileFound onFileFound) {
    switch (mode) {
      case OTG:
        OTGUtil.getDocumentFiles(path, context, onFileFound);
        break;
      case DOCUMENT_FILE:
        OTGUtil.getDocumentFiles(SafRootHolder.getUriRoot(), path, context, OpenMode.DOCUMENT_FILE, onFileFound);
        break;
      case DROPBOX:
      case BOX:
      case GDRIVE:
      default:
        ListFilesCommand.INSTANCE.listFiles(
            path,
            isRoot,
            true,
            openMode -> null,
            hybridFileParcelable -> {
              onFileFound.onFileFound(hybridFileParcelable);
              return null;
            });
    }
  }

  /**
   * Helper method to list children of this file
   *
   * @deprecated use forEachChildrenFile()
   */
  public ArrayList<HybridFileParcelable> listFiles(Context context, boolean isRoot) {
    ArrayList<HybridFileParcelable> arrayList = new ArrayList<>();
    switch (mode) {
      case OTG:
        arrayList = OTGUtil.getDocumentFilesList(path, context);
        break;
      case DOCUMENT_FILE:
        final ArrayList<HybridFileParcelable> hybridFileParcelables = new ArrayList<>();
        OTGUtil.getDocumentFiles(
            SafRootHolder.getUriRoot(),
            path,
            context,
            OpenMode.DOCUMENT_FILE,
            file -> hybridFileParcelables.add(file));
        arrayList = hybridFileParcelables;
        break;
      case DROPBOX:
      case BOX:
      case GDRIVE:
      default:
        arrayList = RootHelper.getFilesList(path, isRoot, true);
    }

    return arrayList;
  }

  public String getReadablePath(String path) {
    if (isSftp() || isSmb()) return parseAndFormatUriForDisplay(path);
    else return path;
  }

  public static String parseAndFormatUriForDisplay(@NonNull String uriString) {
    Uri uri = Uri.parse(uriString);
    return formatUriForDisplayInternal(uri.getScheme(), uri.getHost(), uri.getPath());
  }

  private static String formatUriForDisplayInternal(
      @NonNull String scheme, @NonNull String host, @NonNull String path) {
    return String.format("%s://%s%s", scheme, host, path);
  }

  /**
   * Handles getting input stream for various {@link OpenMode}
   *
   * @deprecated use {@link #getInputStream(Context)} which allows handling content resolver
   */
  @Nullable
  public InputStream getInputStream() {
    InputStream inputStream;
    try {
      inputStream = new FileInputStream(path);
    } catch (FileNotFoundException e) {
      inputStream = null;
    }
    return inputStream;
  }

  @Nullable
  public InputStream getInputStream(Context context) {
    InputStream inputStream;

    switch (mode) {
      case DOCUMENT_FILE:
        ContentResolver contentResolver = context.getContentResolver();
        DocumentFile documentSourceFile = getDocumentFile(false);
        try {
          inputStream = contentResolver.openInputStream(documentSourceFile.getUri());
        } catch (FileNotFoundException e) {
          inputStream = null;
        }
        break;
      case OTG:
        contentResolver = context.getContentResolver();
        documentSourceFile = OTGUtil.getDocumentFile(path, context, false);
        try {
          inputStream = contentResolver.openInputStream(documentSourceFile.getUri());
        } catch (FileNotFoundException e) {
          inputStream = null;
        }
        break;
      case DROPBOX:
      case BOX:
      case GDRIVE:
      default:
        try {
          inputStream = new FileInputStream(path);
        } catch (FileNotFoundException e) {
          inputStream = null;
        }
        break;
    }
    return inputStream;
  }

  @Nullable
  public OutputStream getOutputStream(Context context) {
    OutputStream outputStream;
    switch (mode) {
      case DOCUMENT_FILE:
        ContentResolver contentResolver = context.getContentResolver();
        DocumentFile documentSourceFile = getDocumentFile(true);
        try {
          outputStream = contentResolver.openOutputStream(documentSourceFile.getUri());
        } catch (FileNotFoundException e) {
          outputStream = null;
        }
        break;
      case OTG:
        contentResolver = context.getContentResolver();
        documentSourceFile = OTGUtil.getDocumentFile(path, context, true);
        try {
          outputStream = contentResolver.openOutputStream(documentSourceFile.getUri());
        } catch (FileNotFoundException e) {
          outputStream = null;
        }
        break;
      default:
        try {
          outputStream = FileUtil.getOutputStream(getFile(), context);
        } catch (Exception e) {
          outputStream = null;
        }
    }
    return outputStream;
  }

  public boolean exists() {
    boolean exists = false;
    if (isLocal()) {
      exists = getFile().exists();
    } else if (isRoot()) {
      return RootHelper.fileExists(path);
    }
    return exists;
  }

  /** Helper method to check file existence in otg */
  public boolean exists(Context context) {
    boolean exists = false;
    try {
      if (isOtgFile()) {
        exists = OTGUtil.getDocumentFile(path, context, false) != null;
      } else if (isDocumentFile()) {
        exists =
            OTGUtil.getDocumentFile(
                    path, SafRootHolder.getUriRoot(), context, OpenMode.DOCUMENT_FILE, false)
                != null;
      } else return (exists());
    } catch (Exception e) {
    }
    return exists;
  }

  /**
   * Whether file is a simple file (i.e. not a directory/smb/otg/other)
   *
   * @return true if file; other wise false
   */
  public boolean isSimpleFile() {
    return !isSmb()
        && !isOtgFile()
        && !isDocumentFile()
        && !isCustomPath()
        && !android.util.Patterns.EMAIL_ADDRESS.matcher(path).matches()
        && (getFile() != null && !getFile().isDirectory())
        && !isOneDriveFile()
        && !isGoogleDriveFile()
        && !isDropBoxFile()
        && !isBoxFile()
        && !isSftp();
  }

  public boolean setLastModified(final long date) {
    if (isSmb()) {
      try {
        SmbFile smbFile = getSmbFile();
        if (smbFile != null) {
          smbFile.setLastModified(date);
          return true;
        } else {
          return false;
        }
      } catch (SmbException e) {
        return false;
      }
    }
    File f = getFile();
    return f.setLastModified(date);
  }

  public void mkdir(Context context) {
    if (isOtgFile()) {
      if (!exists(context)) {
        DocumentFile parentDirectory = OTGUtil.getDocumentFile(getParent(context), context, true);
        if (parentDirectory.isDirectory()) {
          parentDirectory.createDirectory(getName(context));
        }
      }
    } else if (isDocumentFile()) {
      if (!exists(context)) {
        DocumentFile parentDirectory =
            OTGUtil.getDocumentFile(
                getParent(context),
                SafRootHolder.getUriRoot(),
                context,
                OpenMode.DOCUMENT_FILE,
                true);
        if (parentDirectory.isDirectory()) {
          parentDirectory.createDirectory(getName(context));
        }
      }
    }  else MakeDirectoryOperation.mkdir(getFile(), context);
  }

  public boolean delete(Context context, boolean rootmode)
      throws ShellNotRunningException, SmbException {
    if (isRoot() && rootmode) {
      setMode(OpenMode.ROOT);
      DeleteFileCommand.INSTANCE.deleteFile(getPath());
    } else {
      DeleteOperation.deleteFile(getFile(), context);
    }
    return !exists();
  }

  /**
   * Returns the name of file excluding it's extension If no extension is found then whole file name
   * is returned
   */
  public String getNameString(Context context) {
    String fileName = getName(context);

    int extensionStartIndex = fileName.lastIndexOf(".");
    return fileName.substring(
        0, extensionStartIndex == -1 ? fileName.length() : extensionStartIndex);
  }

  /**
   * Generates a {@link LayoutElementParcelable} adapted compatible element. Currently supports only
   * local filesystem
   */
  public LayoutElementParcelable generateLayoutElement(@NonNull Context c, boolean showThumbs) {
    switch (mode) {
      case FILE:
      case ROOT:
        File file = getFile();
        LayoutElementParcelable layoutElement;
        if (isDirectory(c)) {

          layoutElement =
              new LayoutElementParcelable(
                  c,
                  path,
                  RootHelper.parseFilePermission(file),
                  "",
                  folderSize() + "",
                  0,
                  true,
                  file.lastModified() + "",
                  false,
                  showThumbs,
                  mode);
        } else {
          layoutElement =
              new LayoutElementParcelable(
                  c,
                  file.getPath(),
                  RootHelper.parseFilePermission(file),
                  file.getPath(),
                  file.length() + "",
                  file.length(),
                  false,
                  file.lastModified() + "",
                  false,
                  showThumbs,
                  mode);
        }
        return layoutElement;
      default:
        return null;
    }
  }


  public void getMd5Checksum(Context context, Function<String, Void> callback) {
    Single.fromCallable(
            () -> {
              try {
                switch (mode) {
                  case SFTP:

                  default:
                    byte[] b = createChecksum(context);
                    String result = "";

                    for (byte aB : b) {
                      result += Integer.toString((aB & 0xff) + 0x100, 16).substring(1);
                    }
                    return result;
                }
              } catch (Exception e) {
                return context.getString(R.string.error);
              }
            })
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(
            new SingleObserver<String>() {
              @Override
              public void onSubscribe(Disposable d) {}

              @Override
              public void onSuccess(String t) {
                callback.apply(t);
              }

              @Override
              public void onError(Throwable e) {
                callback.apply(context.getString(R.string.error));
              }
            });
  }

  public void getSha256Checksum(Context context, Function<String, Void> callback) {
    Single.fromCallable(
            () -> {
              try {
                switch (mode) {
                  default:
                    MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
                    byte[] input = new byte[GenericCopyUtil.DEFAULT_BUFFER_SIZE];
                    int length;
                    InputStream inputStream = getInputStream(context);
                    while ((length = inputStream.read(input)) != -1) {
                      if (length > 0) messageDigest.update(input, 0, length);
                    }

                    byte[] hash = messageDigest.digest();

                    StringBuilder hexString = new StringBuilder();

                    for (byte aHash : hash) {
                      // convert hash to base 16
                      String hex = Integer.toHexString(0xff & aHash);
                      if (hex.length() == 1) hexString.append('0');
                      hexString.append(hex);
                    }
                    inputStream.close();
                    return hexString.toString();
                }
              } catch (IOException | NoSuchAlgorithmException ne) {
                return context.getString(R.string.error);
              }
            })
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(
            new SingleObserver<String>() {
              @Override
              public void onSubscribe(Disposable d) {}

              @Override
              public void onSuccess(String t) {
                callback.apply(t);
              }

              @Override
              public void onError(Throwable e) {
                callback.apply(context.getString(R.string.error));
              }
            });
  }

  private byte[] createChecksum(Context context) throws Exception {
    InputStream fis = getInputStream(context);

    byte[] buffer = new byte[8192];
    MessageDigest complete = MessageDigest.getInstance("MD5");
    int numRead;

    do {
      numRead = fis.read(buffer);
      if (numRead > 0) {
        complete.update(buffer, 0, numRead);
      }
    } while (numRead != -1);

    fis.close();
    return complete.digest();
  }

  private void openFileInternal(Context activity) {

  }
}
